Elementi a supporto della Programmazione funzionale

Vediamo di seguito quelli che sono gli strumenti più usati per programmare con uno stile elegante, preciso e conciso.

Lambda Expressions

Il primo elemento sono le lambda expressions, chiamate anche anonymous functions. La loro forma generale è del tipo:

`lambda : `

Dove lambda è una parola riservata (keyword) di Python. Le lambda expressions sono usate soprattutto quando si devono passare delle funzioni come parametri ad altre funzioni.

ESEMPIO:


In [ ]:
# Due modi diversi per definire la stessa funzione:
# Primo: metodo standard, funzione di nome F1
def F1(x):
    return x**2

# Secondo: lambda expression a cui si assegna un nome
F2 = lambda x: x**2

F1(3.3) == F2(3.3)

In [ ]:
(lambda x,y: x+y)(2,3)

In [ ]:
print(type(lambda x: x**2))

Funzione map

La funzione map è la prima di una triade di funzioni molto importanti. Le altre due sono filter e reduce.

La funzione map è una funzione che prende in input come primo argomento una funzione di $n$ argomenti e viene poi seguita da $n$ collezioni iterabili della stessa lunghezza (come ad esempio $n$ liste); restituisce in output generator specifico di tipo map object:

`map(func, *iterables) --> map object`

Per ottenere il risultato bisogna iterare sugli elementi del map object, per esempio costruendo una lista con la funzione list(). Di solito la funzione data come prima argomento prende un solo argomento in input e la funzione viene quindi usata con una sola lista di input. La funzione map è una delle builtins di Python, ma è relativamente facile scrivere una versione usando gli elementi di Python stesso.

ESEMPIO 1: Per calcolare il quadrato di una lista di numeri:


In [ ]:
map(lambda x: x**2, [1,2,3,4,5,6,7,8,9,10])

In [ ]:
list(map(lambda x: x**2, [1,2,3,4,5,6,7,8,9,10]))

Si osservi che la funzione map ha passato alla funzione list un map object con cui ha costruito la lista data visualizzata in output.

ESEMPIO 2: Per calcolare il prodotto elemento per elemento di due vettori:


In [ ]:
list(map(lambda x, y: x*y, [1,2,3,4,5,6,7,8,9], [9,8,7,6,5,4,3,2,1]))

ESERCIZIO 1: Scrivere un'espressione che calcoli il quadrato delle differenze elemento per elemento di due "vettori" dati, ovvero: $$\sum_{i = 1,..,n} (x_i-y_i)^2, \quad x,y \in \mathbb{R}^n$$

Usare nell'espressione le due liste $[1,2,3,4,5,6,7,8,9]$ e $[9,8,7,6,5,4,3,2,1]$.


In [ ]:
# SVOLGERE ESERCIZIO

Funzione filter

La funzione filter letteralmente "filtra" gli elementi: prende in input un predicato (ovvero una funzione booleana, che restituisce True o False) e una sequenza iterabile di elementi, e restituisce in output un filter object su cui iterando si ottiene la sequenza di elementi per cui il predicato è verificato (pari a True):

`filter(function or None, iterable) --> filter object`

La funzione filter è una delle builtins di Python.

ESEMPIO 3: Filtrare i numeri pari di una lista data.


In [ ]:
list(filter(lambda x: x%2 == 0, {1,2,3,4,5,6,7,8,9}))

ESERCIZIO 2: Calcolare il quadrato dei numeri dispari di una lista data. Suggerimento: utilizzare sia map che filter.


In [ ]:
# SVOLGERE ESERCIZIO

List comprehensions

Usare la notazione di list comprehensions può rendere talvolta il codice più compatto e può spostare il focus di chi lo legge dal come si stan facendo i conti al cosa si sta calcolando. Si consideri l'esempio seguente:

collection = list()
for datum in data_set:
    if condition(datum):
        collection.append(datum)
    else:
        new = modify(datum)
        collection.append(new)

che usando la notazione di list comprehensions si può riscrivere come:

collection = [d if condition(d) else modify(d) for d in data_set]

Funzione reduce

La funzione reduce letteralmente "riduce" una sequenza di elementi ad uno scalare. In programmazione funzionale viene chiamata anche fold. La funzione reduce prende in input una funzione, chiamata "combinante", una sequenza iterabile di elementi e un valore iniziale (facoltativo). In output viene restituito un valore che risulta dall'applicare in sequenza agli elementi della lista la funzione data:

`reduce(function, sequence[, initial]) -> value`

Per esempio, se viene passata in input la funzione f(x,y), la lista [1,2,3,4] e il valore iniziale 0, la funzione reduce calcola il valore:

$$v = f(f(f(f(0,1), 2), 3), 4)$$

Se la funzione data è la somma, questo risulta equivalente a calcolare:

$$((((0+1)+2)+3)+4) = 10$$

La funzione reduce non è una builtin di Python e deve essere importata con il comando:

from functools import reduce

ESEMPIO 4: Scrivere il codice per l'esempio precedente


In [ ]:
from functools import reduce

In [ ]:
reduce(lambda x,y: x+y, [1,2,3,4])

ESERCIZIO 3: Scrivere una funzione che calcoli la norma di un vettore $\sqrt{\sum_{i=1,..,n} x_i^2}$. Suggerimento: la funzione sqrt è una funzione della libreria math e deve essere importata con ìl comando from math import sqrt.


In [ ]:
from math import sqrt

In [ ]:
# SVOLGERE ESERCIZIO

4. Lazy Evaluation e Liste infinite

Negli esempi sopra, le funzioni map e filter erano funzioni che restituivano delle lista che venivano elaborate in maniera LAZY: ovvero, l'elemento i-esimo della lista non sono calcolato sino a quando non è effettivamente necessario per effettuare qualche calcolo. Per questo motivo, si dice che la lista viene valutata in modalità lazy. Questo è un concetto molto generale, che in Python viene implementato attraverso degli oggetti chiamati generators e iterators. I primi sono gli oggetti che generano le liste, mentre i secondi sono gli oggetti che permettono di valutare tali liste in modalità lazy, valutando un elemento alla volta solo quando viene effettivamente richiesto. Per poter definire una funzione che genera una lista infinita si usa la parola chiave yield invece del solito return. Per richiedere un elemento alla volta di un iteratore si utilizza la builtin next() (vedere l'esempio sotto).

Si noti, che sfruttando una valutazione delle liste di tipo LAZY è quindi possibile definire delle liste di lunghezza infinita, che però vengono valutate solamente quando uno dei suoi elementi è veramente necessario per effettuare dei conti.

ESEMPI:

  1. Funzione Counter() che restituisce la lista infinita dei numeri natuali a partire da 1.
  2. Funzione builtin Enumerate(Ls) che restituisce la coppia (i,l) dove l è l'i-esimo elemento della lista Ls.

In [ ]:
# Primo esempio di lista "infinita"
def Counter():
    c = 1
    while True:
        yield c
        c += 1
        
cnt = Counter()
print(type(cnt))
print([next(cnt) for _ in range(10)])

In [ ]:
# Implementazione naive in Python della funzione builtin `enumerate(Ls)`
def Enumerate(Ls):
    i = 0
    for l in Ls:
        yield i,l
        i += 1

print(Enumerate("CiaoBella"))
print(list(Enumerate("CiaoBella")))

ESERCIZIO 4: Implementare una funzione che restituisce lista infinita dei numeri primi primi.


In [ ]:
# Terzo esempio di lista infinita
def NumeriPrimi():
    # DA COMPLETARE

crivello = NumeriPrimi()        
print([next(crivello) for _ in range(10)])